<?php
// vim: syntax=php 
/**
 * Drupal OpenID Consumer Plugin
 *
 * Copyright 2006 JanRain, Inc.
 * Copyright 2007 Justin Gallardo
 *
 * This module implements consumer support for OpenID authentication.
 * Using this module, OpenID users can log into a Drupal installation.
 *
 * Some notes:
 *
 * - An OpenID login will result in an automatic account creation if
 * that OpenID has not previously been used to authenticate to the
 * Drupal installation.
 *
 * - OpenID users will be prompted for an email address and username before a
 * Drupal account is created.
 *
 * - This module only works with Drupal when it runs on MySQL.
 *
 * - This module will create three tables in the Drupal MySQL
 * database, and the names of all three will be prefixed with
 * "openid_" and a fourth named 'dbprefix_openid'.
 */

/**
 * Notes:
 *
 * The form field name for the OpenID login form is edit[openid_url],
 * as generated by Drupal.  If we can find a way to fix this, it
 * should instead be openid_url.
 */

really_require_once('Auth/OpenID/Consumer.php');
really_require_once('Auth/OpenID/MySQLStore.php');
really_require_once('Auth/OpenID/DatabaseConnection.php');

/**
 * This function replaces require_once and attempts to fix a PHP bug
 * that interferes with require_once behavior when it is used inside a
 * function.  require_once, when used inside a function, only puts
 * functions and classes into the global namespace; global variables
 * in the required file are put into the local scope of the function
 * call, which is not what is desired.  Because this module is
 * required in the scope of another function, the bug occurs and we
 * have to put things into the global namespace manually.  Whee!
 */
function really_require_once($path) {
    require_once($path);
    foreach (get_defined_vars() as $k => $v) {
        $GLOBALS[$k] = $v;
    }
}

/**
 * An empty auth hook so this module shows up as an auth module.  The
 * real work is done by openid_login_form_submit and openid_return.
 */
function openid_auth($username, $password, $server){
    return;
}


/**
 * Provides an OpenID login form as a block.
 */
function openid_block($op='list', $delta=0) {
    global $user;
    // listing of blocks, such as on the admin/block page

    if ($op == "list") {
        $blocks[0]["info"] = t("OpenID Login");
        return $blocks;
    } else if ($op == 'view' && (!$user->uid)) {
	drupal_add_css(drupal_get_path('module', 'openid') . '/openid.css');
        $block['subject'] = t("OpenID Login");
        $block['content'] = drupal_get_form("openid_login_create_form");
        return $block;
    }
}

/**
 * Adds an OpenID field to the user profile page.
 */

function openid_user($op, &$edit, &$user, $category = NULL){
	switch($op){
	  case 'form':
	  	if($category == 'account'){
			return openid_user_form($edit, $user, $category);
		}
		break;
	  case 'view':
	  	if($openid = openid_view($user->uid)){
			$form['openid'] = array(
			  'value' => $openid,
			  );
			  return array(t('OpenID') => $form);
		}
		break;
	  case 'delete':
	  	return openid_delete($user->uid);
		break;
	  case 'update':
	  	return openid_update($user->uid, $edit);
		break;
	}
}

/**
 * This runs when a user's profile is viewed. We use this to display our user's OpenID.
 */

function openid_view($uid){
	$openid_cat = array();
	$result = db_query("SELECT openid FROM {openid} WHERE uid = %d", $uid); 
	while ($row = db_fetch_object($result)){ 
		if(preg_match('/^http:\/\/xri.net/', $row->openid)){
			$openid_work = preg_replace('/^http:\/\/xri.net\/=/', '=', $row->openid);
			$openid_cat[] = rtrim($openid_work, '/');
		}
		else{
			$openid_cat[] = $row->openid;
		}
	}
	
	$openid = implode("<br />", $openid_cat);
	
	if($openid){
		return $openid;
	}
	else{
		return '';
	}
}

/**
 * This runs when a user is deleted. We want to be sure their entry is deleted from the openid table.
 */

function openid_delete($uid){
	db_query("DELETE FROM {openid} WHERE uid = %d", $uid);
}

/**
 * This runs when a user updates their profile. This way we can make sure their OpenID is up to date.
 */
 
function openid_update($uid, &$edit){
	$openidField = $edit['openid'];
	$openids = explode("\r\n",$openidField);
	$validated = 1;
	if($openidField){
		foreach($openids as $openid){
			if($openid){
				if(preg_match('/^=/', $openid)){
					$openid = preg_replace('/^=/', 'http://xri.net/=', $openid);
				}
				
				$openid = openid_normalize_url($openid);
				if($row = db_fetch_object(db_query("SELECT * FROM {openid} WHERE openid = '%s'", $openid))){
					if($row->uid != $uid){
						$validated = 0;
						drupal_set_message("That OpenID is taken already. Please try again.", 'error');
						header("Location: " . url('user/' . $uid . '/edit'));
						exit(0);
					}
				}
			}
			else{
				$validated = 0;
				drupal_set_message("That OpenID isn't valid. Please try again.", 'error');
				header("Location: " . url('user/' . $uid . '/edit'));
				exit(0);
			}
				
		}
		
		if($validated == 1){
			db_query("DELETE FROM {openid} WHERE uid = '%d'", $uid);
			
			foreach($openids as $openid){
				if(preg_match('/^=/', $openid)){
					$openid = preg_replace('/^=/', 'http://xri.net/=', $openid);
				}
				$openidClean = openid_normalize_url($openid);
				db_query("INSERT INTO {openid} VALUES ('%d', '%s')", $uid, $openidClean);
			}
		}
	}
	else{
		db_query("DELETE FROM {openid} WHERE uid = '%d'", $uid);
	}
	
	unset($edit['openid']);
}

/**
 * Creates the form used for grabbing a user's OpenID.
 */

function openid_user_form($edit, $user, $category){
	$openidCat = array();
	$result = db_query("SELECT openid FROM {openid} WHERE uid = '%d'", $user->uid); 
	while ($row = db_fetch_object($result)){ 
		if(preg_match('/^http:\/\/xri.net/', $row->openid)){
			$openidWork = preg_replace('/^http:\/\/xri.net\/=/', '=', $row->openid);
			$openidCat[] = preg_replace('/\/$/', '', $openidWork);
		}
		else{
			$openidCat[] = $row->openid;
		}
	}
	
	$openid = implode("\n", $openidCat);
		
	$row = db_fetch_object(db_query("SELECT openid FROM {openid} WHERE uid = '%d'", $user->uid));
	$form['openid'] = array(
	  '#type' => 'fieldset',
	  '#collapsible' => FALSE,
	  '#collapsed' => FALSE,
	  '#weight' => 1,
	  );
	$form['openid']['openid'] = array(
	  '#type' => 'textarea',
	  '#title' => t('OpenID'),
	  '#default_value' => $openid,
	  '#description' => 'Enter your OpenIDs, one per line.',
	  );
	return $form;
}

/**
 * Creates the OpenID Login Form
 */
function openid_login_create_form(){
	$openid_affiliate = variable_get('openid_affiliate', '');
	$form['openid_url'] = array(
	  '#type' => 'textfield',
	  '#title' => t("OpenID"),
	  '#default_value' => '',
	  '#size' => 15,
	  '#maxlength' => 64,
	  '#required' => TRUE,
	  '#description' => t("Still don't have an OpenID? Get one <a href=\"http://www.myopenid.com/".($openid_affiliate?"affiliate_signup?affiliate_id=$openid_affiliate":"")."\">here</a>.")
	  );
	
	$form['#action'] = url('openid/login');
	
	$form['submit'] = array(
	  '#type' => 'submit',
	  '#value' => t('Log In')
	  );
	
	return $form;
}

/**
 * Define paths that will be accessed through Drupal.
 *
 * openid/login: Handles form submit from openid_block
 * openid/return: handles redirect return from identity server
 * openid/get_email: Gets email from an OpenID-authenticated user if
 * the user has not logged in before.
 */
function openid_menu($may_cache) {
    $items = array();

    $items[] = array('path' => 'admin/settings/openid', 'title' => t('OpenID'),
    		     'callback' => 'drupal_get_form', 'callback arguments' => 'openid_admin',
		     'access' => user_access('access administration pages'),
		     'type' => MENU_NORMAL_ITEM,
		     );
    $items[] = array('path' => 'openid/get_email', 'title' => t('OpenID User Creation'),
                     'callback' => 'openid_email_form', 'type' => MENU_CALLBACK,
                     'access' => user_access('access content'));
    $items[] = array('path' => 'openid/submit_email', 'title' => t('email form submit'),
                     'callback' => 'openid_create_account_submit', 'type' => MENU_CALLBACK,
                     'access' => user_access('access content'));
    $items[] = array('path' => 'openid/login', 'title' => t('openid login'),
                     'callback' => 'openid_login_form_submit', 'type' => MENU_CALLBACK,
                     'access' => user_access('access content'));
    $items[] = array('path' => 'openid/return', 'title' => t('openid return'),
                     'callback' => 'openid_return', 'type' => MENU_CALLBACK,
                     'access' => user_access('access content'));

    $items[] = array('path' => 'user/openid', 'title' => t('OpenId'),
    		     'callback' => 'drupal_get_form','callback arguments' => 'openid_login_create_form',
		     'access' => !$user->uid, 'type' => MENU_LOCAL_TASK);

    return $items;
}

/**
 * Creates the admin settings form to allow the user to set settings like a MyOpenID affiliate ID 
 */

function openid_admin(){
	$form['openid_affiliate'] = array(
	  '#type' => 'textfield',
	  '#title' => t('MyOpenID Affiliate ID'),
	  '#default_value' => variable_get('openid_affiliate', ''),
	  '#size' => 40,
	  '#maxlength' => 64,
	  '#description' => t("Enter your MyOpenID affiliation ID here."),
	  );

	$form['openid_skipregistration'] = array(
	  '#type' => 'checkbox',
	  '#title' => t('Skip Registration Form'),
	  '#default_value' => variable_get('openid_skipregistration', ''),
	  '#description' => t('If you want new users to skip the registration page, using the OpenID as the username, and trying to pull the email address from the persona, check this box.'),
	  );
	
	return system_settings_form($form);
}

/**
 * Defines the permissions available for the different menu callbacks.
 */

function openid_perm(){
	return array('access content');
}

/**
 * Callback for the OpenID login form.  This is called when the OpenID
 * login form is submitted.  This begins the OpenID authentication
 * process and redirects the user's browser to the OpenID server.
 */
function openid_login_form_submit() {

    $query = array_merge( $_GET, $_POST );
    $edit = $query['edit'];
    $op = $query['op'];
    $dest = $query['destination'];
    
    // Detect if we are being returned from someone signing up with the affiliate program
    if($query['openid_identifier']){
    	$identity_url = $query['openid_identifier'];
    }
    else{	// i-name support, as requested by many.	
	if(preg_match('/^=/', $query['openid_url'])){
		$identity_url = preg_replace('/^=/', 'http://xri.net/=', $query['openid_url']);
	}
	else{	
		$identity_url = $query['openid_url'];
	}
    }
    $previous_error_level = variable_get('error_level', 0);
    variable_set('error_level', 0);
    $consumer = new Auth_OpenID_Consumer(new OpenID_DrupalStore());
    $auth_request = $consumer->begin($identity_url);
    variable_set('error_level', $previous_error_level);
    if ($auth_request) {
        $trust_root = url(NULL, NULL, NULL, TRUE);

        // Add simple registration arguments, but only if the URL
        // being auth'd doesn't exist in the db.
        if (!user_get_authmaps(Auth_OpenID::normalizeUrl($identity_url))) {
            $auth_request->addExtensionArg('sreg', 'optional', 'email,nickname');
            $auth_request->addExtensionArg('sreg', 'policy_url', 'http://example.com/policy');
        }
        
	$app_url = url('openid/return', NULL, NULL, TRUE);

        $redirect_url = $auth_request->redirectURL($trust_root, $app_url);
        header( 'Location: ' . $redirect_url );
        exit;
    } else {
        drupal_set_message('OpenID authentication failed: '.
                           'not a valid OpenID URL.', 'error');
    }

    drupal_goto();
}

/**
 * Callback for return_to url redirection. The identity server will
 * redirect back to this handler with the results of the
 * authentication attempt.  If the authentication succeeds and the
 * user does not have a drupal database account, an email form will be
 * presented.  Else, the user will be logged in.
 */
function openid_return() {

    $query = array_merge( $_GET, $_POST );
    $dest = $query['destination'];

    $previous_error_level = variable_get('error_level', 0);
    variable_set('error_level', 0);
    $consumer = new Auth_OpenID_Consumer(new OpenID_DrupalStore());

    $response = $consumer->complete($query);
    variable_set('error_level', $previous_error_level);

    if ($response->status == Auth_OpenID_CANCEL) {
        drupal_set_message("OpenID authentication cancelled.", 'error');
    } else if ($response->status != Auth_OpenID_SUCCESS) {
        drupal_set_message("OpenID authentication failed (".$response->status.", ".$response->message.").", 'error');
    } else { // SUCCESS.
        global $user;

        // Look for sreg data.
        $sreg = $response->extensionResponse('sreg');

        $openid_url = openid_normalize_url($response->identity_url);
        $destination = $query['destination'];
        
	$row = db_fetch_object(db_query("SELECT * FROM {openid} WHERE openid = '%s'", $openid_url));
	if($row->uid){
	    $user = user_load(array( 'uid' => $row->uid ));
            watchdog('user', t('External load by %user using module %module.',
                               array('%user' => theme('placeholder', $openid_url),
                                     '%module' => theme('placeholder', 'openid'))));
        } 
	else {
	    if(variable_get('openid_skipregistration')){
		    $user = user_save('', array('name' => $openid_url,
						'pass' => user_password(),
						'mail' => $query['email'],
						'init' => $query['email'],
						'status' => 1,
						'roles' => array(DRUPAL_AUTHENTICATED_RID)));
		    
		    if($user){
			$query = db_query("INSERT INTO {openid} VALUES ('%d','%s')", $user->uid, openid_normalize_url($openid_url));
		    }
	    
		    watchdog('user',
			     t('New external user: %user using module %module.',
			       array('%user' => theme('placeholder', $openid_url),
				     '%module' => theme('placeholder', 'openid'))),
			     WATCHDOG_NOTICE, l(t('edit'),
						'user/'. $user->uid .'/edit'));

		    drupal_goto();
	    }
            $_SESSION['openid'] = $openid_url;

            if ($sreg) {
                $_SESSION['sreg_email'] = $sreg['email'];
		$_SESSION['sreg_nickname'] = $sreg['nickname'];
            }
            // Give the user a page that will request an email
            // address before creating the drupal account.
            header("Location: " . url('openid/get_email'));
            exit;
        }
    }

    drupal_goto(); # $dest);
}

/**
 * Presents an email form to an OpenID-authenticated user whose OpenID
 * does not correspondt to an existing Drupal account.
 */
function openid_email_form() {

    $form = array();

    if (!array_key_exists('openid', $_SESSION)) {
        drupal_set_message("OpenID authentication required.", 'error');
        drupal_goto();
        exit;
    }

    $content = sprintf("<h3>Create account with OpenID</h3><p>Before logging in " .
                       "with your OpenID (%s), we need a little information for you new account:</p> %s",
                       $_SESSION['openid'], drupal_get_form('openid_email_create_form'));

    print theme('page', $content);
}

/**
 * Creates the email address create form.
 */

function openid_email_create_form(){
	$form['nickname'] = array(
	  '#type' => 'textfield',
	  '#title' => t('Username'),
	  '#default_value' => @$_SESSION['sreg_nickname'],
	  '#size' => 25,
	  '#maxlength' => 64,
	  '#required' => TRUE,
	  '#description' => 'Enter your desired username.',
	  );
	
	$form['email'] = array(
	  '#type' => 'textfield',
	  '#title' => t('Email Address'),
	  '#default_value' => @$_SESSION['sreg_email'],
	  '#size' => 25,
	  '#maxlength' => 64,
	  '#required' => TRUE,
	  '#description' => 'Enter your email address.'
	  );
	
	$form['#action'] = url('openid/submit_email');
	
	$form['submit'] = array(
	  '#type' => 'submit',
	  '#value' => 'Submit'
	  );
	
	return $form;
	  
}

/**
 * Once an OpenID-authenticated user submits an email address, this is
 * called and is used to create the user's account and log them in.
 */
function openid_create_account_submit() {

    global $user;

    $query = $_POST;
    
    if (user_validate_name($query['nickname']) != NULL){
	drupal_set_message('Username invalid.', 'error');
	header("Location: " . url('openid/get_email'));
	exit(0);
    }
    if (!valid_email_address($query['email'])) {
        drupal_set_message('Email address invalid.', 'error');
        header("Location: " . url('openid/get_email'));
        exit(0);
    }

    if (user_load(array('name' => $query['nickname']))){
    	drupal_set_message('Username exists. Please try again', 'error');
	header("Location: " . url('openid/get_email'));
	exit(0);
    }

    if (user_load(array('mail' => $query['email']))){
    	drupal_set_message('This email account already exists. Please log in using your existing password and add in your OpenID URL through the My Account option.', 'error');
	header("Location: " . url('openid/get_email'));
	exit(0);
    }
    // Process a form submission for an openid-authenticated user and
    // create an account for them.

    $user = user_save('', array('name' => $query['nickname'],
                                'pass' => user_password(),
                                'mail' => $query['email'],
                                'init' => $query['email'],
                                'status' => 1,
                                'roles' => array(DRUPAL_AUTHENTICATED_RID)));
    
    if($user){
    	$query = db_query("INSERT INTO {openid} VALUES ('%d','%s')", $user->uid, openid_normalize_url($_SESSION['openid']));
    }
    
    watchdog('user',
             t('New external user: %user using module %module.',
               array('%user' => theme('placeholder', $_SESSION['openid']),
                     '%module' => theme('placeholder', 'openid'))),
             WATCHDOG_NOTICE, l(t('edit'),
                                'user/'. $user->uid .'/edit'));

    unset($_SESSION['openid']);

    drupal_goto();
}


/**
 * Normalizes URLs so we can be sure they are all formed the same way.
 */

function openid_normalize_url($url){
	$normalized_url = $url;
	if(stristr($url, '://') === FALSE){
		$normalized_url = 'http://' . $url;
	}
	if(!preg_match('/\/$/', $normalized_url)){
		$normalized_url .= '/';
	}
	
	return $normalized_url;
}

/**
 * Emulates a PEAR database connection which is what the Auth_OpenID
 * library expects to use for its database storage.  This just wraps
 * Drupal's database abstraction calls.
 */
class Drupal_OpenID_DBConnection extends Auth_OpenID_DatabaseConnection {

    function setFetchMode($mode) {
        // Not implemented; returned results will emulate PEAR's
        // DB_FETCHMODE_ASSOC fetch mode.
    }

    function autoCommit($mode) {
        // Not implemented.
    }

    function query($sql, $params = array()) {
        return db_query($sql, $params);
    }

    function getOne($sql, $params = array()) {
        $result = db_query($sql, $params);

        if ($result === false) {
            return false;
        } else {
            $row = db_fetch_array($result);
            $keys = array_keys($row);
            return $row[$keys[0]];
        }
    }

    function getRow($sql, $params = array()) {
        $result = db_query($sql, $params);

        if ($result === false) {
            return false;
        } else {
            return db_fetch_array($result);
        }
    }

    function getAll($sql, $params = array()) {
        $result = db_query($sql, $params);

        if ($result === false) {
            return false;
        } else {
            $result_rows = array();
            for ($i = 0; $i < db_num_rows($result); $i++) {
                $result_rows[] = db_fetch_array($result);
            }
            return $result_rows;
        }
    }
}

/**
 * A drupal-compatible store implementation that uses the
 * Drupal_OpenID_DBConnection.
 */
class OpenID_DrupalStore extends Auth_OpenID_MySQLStore {

    function OpenID_DrupalStore()
    {
        $conn = new Drupal_OpenID_DBConnection();
        parent::Auth_OpenID_MySQLStore($conn,
                                       'openid_consumer_settings',
                                       'openid_consumer_associations',
                                       'openid_consumer_nonces');
        $this->createTables();
    }

    /**
     * Returns true if the specified value is considered an error
     * value.  Values returned from database calls will be passed to
     * this function to make decisions.
     */
    function isError($value)
    {
        return $value === false;
    }

    /**
     * This function is responsible for encoding binary data to make
     * it safe for use in SQL.
     */
    function blobEncode($blob)
    {
        return $blob;
    }

    /**
     * Given encoded binary data, this function is responsible for
     * decoding it from its encoded representation.  Some backends
     * will not return encoded data, like this one, so no conversion
     * is necessary.
     */
    function blobDecode($blob)
    {
        return $blob;
    }

    /**
     * This is called by the SQLStore constructor and gives the
     * implementor a place to specify SQL that is used to store and
     * retrieve specific data.  Here we call the parent class's setSQL
     * to get the basic SQL data for a MySQL store, but we redefine
     * some of the SQL statements to use Drupal-specific placeholders,
     * since "?" and "!" (PEAR placeholders) aren't recognized by
     * Drupal's database abstraction code.  Furthermore, things like
     * "%b" for binary values are supported by Drupal, so we use those
     * here.
     */
    function setSQL()
    {
        parent::setSQL();

        $this->sql['create_auth'] =
            "INSERT INTO %s VALUES ('auth_key', '%%s')";

        $this->sql['get_auth'] =
            "SELECT value FROM %s WHERE setting = 'auth_key'";

        $this->sql['set_assoc'] =
            "REPLACE INTO %s VALUES ('%%s', '%%s', '%%s', %%d, %%d, '%%s')";

        $this->sql['get_assocs'] =
            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
            "WHERE server_url = '%%s'";

        $this->sql['get_assoc'] =
            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
            "WHERE server_url = '%%s' AND handle = '%%s'";

        $this->sql['remove_assoc'] =
            "DELETE FROM %s WHERE server_url = '%%s' AND handle = '%%s'";

        $this->sql['add_nonce'] =
            "REPLACE INTO %s (nonce, expires) VALUES ('%%s', %%d)";

        $this->sql['get_nonce'] =
            "SELECT * FROM %s WHERE nonce = '%%s'";

        $this->sql['remove_nonce'] =
            "DELETE FROM %s WHERE nonce = '%%s'";
    }
}

